package cn.ckx.coding.common.utils.hash;

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;

/**
 * 数据内容安全工具
 *
 */
public class RSAUtils {
    /**
     * 数字签名，密钥算法
     */
    private static final String RSA_KEY_ALGORITHM = "RSA";

    /**
     * 数字签名签名/验证算法
     */
    private static final String SIGNATURE_ALGORITHM = "SHA1WithRSA";


    /**
     * RSA密钥长度，RSA算法的默认密钥长度是1024密钥长度必须是64的倍数，在512到65536位之间
     */
    private static final int KEY_SIZE = 2048;


    public static final String PUBLIC_KEY = "RSAPublicKey";

    public static final String PRIVATE_KEY = "RSAPrivateKey";


    private static final Base64.Decoder DECODER = Base64.getDecoder();
    private static final Base64.Encoder ENCODER = Base64.getEncoder();
    private static final Charset CHARSET = StandardCharsets.UTF_8;

    /**
     * <ol>
     *     <li>把Base64编码的RSA公钥（publicKey）、需要被加密的内容（content），转换为字节数组。其中content
     *     使用UTF-8进行转换</li>
     *     <li>生成一个随机字符串（key），并利用此字符串生成一个128位的AES秘钥</li>
     *     <li>使用AES算法对content进行加密，得到密文A</li>
     *     <li>使用RSA算法对key进行加密，得到密文B</li>
     *     <li>依次把密文B长度(int)、密文B、密文B输出到一个数组</li>
     *     <li>使用Base64对结果进行编码，生成字符串</li>
     * </ol>
     *
     * @param publicKey RSA公钥
     * @param content   需要被加密的内容
     * @return 加密后的密文
     * @throws Exception 在publicKey或者content为空（包括空字符串）的时候就会出现此异常
     */
    public static String encrypt(String publicKey, String content) throws Exception {
        if (publicKey == null || publicKey.length() == 0 || publicKey.trim().length() == 0) {
            throw new Exception("加密算法出错:RSA公钥必须提供");
        }
        if (content == null || content.length() == 0 || content.trim().length() == 0) {
            throw new Exception("加密算法出错:被加密的内容不能为空");
        }
        byte[] aesKey = UUID.randomUUID().toString().getBytes();
        byte[] rsaPublicKey = DECODER.decode(publicKey);
        byte[] sourceData = content.getBytes(CHARSET);

        try {
            // 使用AES算法加密原文
            byte[] secretData = aesEncrypt(aesKey, sourceData);

            // 使用RSA算法加密AES秘钥，公钥加密
            byte[] secretKey = rsaPublicKeyEncrypt(rsaPublicKey, aesKey);

            // 把秘钥长度转换为数组
            byte[] secretKeyLength = intToByteArray(secretKey.length);

            // 合并数组并返回
            // 依次把密文B长度、密文B、密文B输出到一个数组
            byte[] data =
                    new byte[secretKeyLength.length + secretKey.length + secretData.length];
            System.arraycopy(secretKeyLength, 0, data, 0,
                    secretKeyLength.length);
            System.arraycopy(secretKey, 0, data, secretKeyLength.length,
                    secretKey.length);
            System.arraycopy(secretData, 0, data,
                    secretKeyLength.length + secretKey.length,
                    secretData.length);
            // 使用Base64对结果进行编码，生成字符串
            return ENCODER.encodeToString(data);
        } catch (Exception e) {
            throw new Exception("加密信息时出现问题,数据加密失败：" + e.getLocalizedMessage()
                    , e);
        }
    }

    /**
     * 私钥加密
     *
     * @param privateKey 私钥
     * @param content 内容
     * @return String
     * @throws Exception 异常
     */
    public static String encryptByPrivateKey(String privateKey,
                                             String content) throws Exception {
        if (privateKey == null || privateKey.length() == 0 || privateKey.trim().length() == 0) {
            throw new Exception("加密算法出错:RSA私钥必须提供");
        }
        if (content == null || content.length() == 0 || content.trim().length() == 0) {
            throw new Exception("加密算法出错:被加密的内容不能为空");
        }
        byte[] aesKey = UUID.randomUUID().toString().getBytes();
        byte[] rsaPrivateKey = DECODER.decode(privateKey);
        byte[] sourceData = content.getBytes(CHARSET);

        try {
            // 使用AES算法加密原文
            byte[] secretData = aesEncrypt(aesKey, sourceData);

            // 使用RSA算法加密AES秘钥，私钥加密
            byte[] secretKey = rsaPrivateKeyEncrypt(rsaPrivateKey, aesKey);

            // 把秘钥长度转换为数组
            byte[] secretKeyLength = intToByteArray(secretKey.length);

            // 合并数组并返回
            // 依次把密文B长度、密文B、密文B输出到一个数组
            byte[] data =
                    new byte[secretKeyLength.length + secretKey.length + secretData.length];
            System.arraycopy(secretKeyLength, 0, data, 0,
                    secretKeyLength.length);
            System.arraycopy(secretKey, 0, data, secretKeyLength.length,
                    secretKey.length);
            System.arraycopy(secretData, 0, data,
                    secretKeyLength.length + secretKey.length,
                    secretData.length);
            // 使用Base64对结果进行编码，生成字符串
            return ENCODER.encodeToString(data);
        } catch (Exception e) {
            throw new Exception("加密信息时出现问题,数据加密失败：" + e.getLocalizedMessage()
                    , e);
        }
    }

    /**
     * 对密文进行解密：
     * <ol>
     *     <li>使用Base64对密文解包，得到数组，把前面4个字节转换为一个int</li>
     *     <li>利用第一步得到的int，分开AES秘钥、内容密文</li>
     *     <li>使用RSA算法结合privateKey解密AES秘钥</li>
     *     <li>使用第三步得到的秘钥，用AES算法解密内容</li>
     *     <li>把解码后得到的数组，使用UTF-8转换为字符串</li>
     * </ol>
     *
     * @param rsaPrivateKey RSA私钥
     * @param secretContent 加密后的密文，用于解密
     * @return 解密后使用UTF-8进行解码
     * @throws Exception 解密时，如果密文或者密钥有问题，都会出现此问题
     */
    public static String decrypt(String rsaPrivateKey, String secretContent)
            throws Exception {
        if (rsaPrivateKey == null || rsaPrivateKey.length() == 0 || rsaPrivateKey.trim().length() == 0) {
            throw new Exception("解密算法出错:RSA私钥必须提供");
        }
        if (secretContent == null || secretContent.length() == 0 || secretContent.trim().length() == 0) {
            throw new Exception("解密算法出错:被解密的内容不能为空");
        }
        try {
            // RSA私钥返回的是Base64编码的，需要解码后使用，此密钥在应用管理中重置密钥时产生。
            byte[] privateKey = DECODER.decode(rsaPrivateKey);
            // 1.Base64解码
            byte[] data = DECODER.decode(secretContent);
            // 2.拆开秘钥和密文
            // AES密钥长度
            byte[] secretKeyLength = Arrays.copyOfRange(data, 0, 4);
            int length = byteArrayToInt(secretKeyLength);
            // RSA加密后的AES密钥
            byte[] secretKey = Arrays.copyOfRange(data, 4, length + 4);
            // AES加密后的通知消息原文
            byte[] secretData = Arrays.copyOfRange(data, length + 4,
                    data.length);
            // 3.使用RSA私钥解密AES秘钥
            byte[] aesKey = rsaPrivateDecrypt(privateKey, secretKey);
            // 4.使用AES秘钥进行解密
            byte[] sourceData = aesDecrypt(aesKey, secretData);
            // 5.使用UTF-8转码
            return new String(sourceData, CHARSET);
        } catch (Exception e) {
            throw new Exception("解密信息时出现问题,数据解密失败：" + e.getLocalizedMessage()
                    , e);
        }

    }

    /**
     * 公钥解密
     *
     * @param rsaPublicKey 公钥
     * @param secretContent 加密内容
     * @return String
     * @throws Exception 异常
     */
    public static String decryptByPublicKey(String rsaPublicKey,
                                      String secretContent)
            throws Exception {
        if (rsaPublicKey == null || rsaPublicKey.length() == 0 || rsaPublicKey.trim().length() == 0) {
            throw new Exception("解密算法出错:RSA私钥必须提供");
        }
        if (secretContent == null || secretContent.length() == 0 || secretContent.trim().length() == 0) {
            throw new Exception("解密算法出错:被解密的内容不能为空");
        }
        try {
            // RSA公钥返回的是Base64编码的，需要解码后使用，此密钥在应用管理中重置密钥时产生。
            byte[] publicKey = DECODER.decode(rsaPublicKey);
            // 1.Base64码
            byte[] data = DECODER.decode(secretContent);
            // 2.拆开秘钥和密文
            // AES密钥长度
            byte[] secretKeyLength = Arrays.copyOfRange(data, 0, 4);
            int length = byteArrayToInt(secretKeyLength);
            // RSA加密后的AES密钥
            byte[] secretKey = Arrays.copyOfRange(data, 4, length + 4);
            // AES加密后的通知消息原文
            byte[] secretData = Arrays.copyOfRange(data, length + 4,
                    data.length);
            // 3.使用RSA公钥解密AES秘钥
            byte[] aesKey = rsaPublicDecrypt(publicKey, secretKey);
            // 4.使用AES秘钥进行解密
            byte[] sourceData = aesDecrypt(aesKey, secretData);
            // 5.使用UTF-8转码
            return new String(sourceData, CHARSET);
        } catch (Exception e) {
            throw new Exception("解密信息时出现问题,数据解密失败：" + e.getLocalizedMessage()
                    , e);
        }

    }

    private static byte[] intToByteArray(int x) {
        byte[] result = new byte[4];
        result[0] = (byte) ((x >> 24) & 0xFF);
        result[1] = (byte) ((x >> 16) & 0xFF);
        result[2] = (byte) ((x >> 8) & 0xFF);
        result[3] = (byte) (x & 0xFF);
        return result;
    }

    private static int byteArrayToInt(byte[] bytes) {
        int value = 0;
        // 由高位到低位
        for (int i = 0; i < bytes.length; i++) {
            int shift = (4 - 1 - i) * 8;
            value += (bytes[i] & 0x000000FF) << shift;
        }
        return value;
    }

    private static byte[] rsaPublicKeyEncrypt(byte[] rsaPublicKey,
                                              byte[] aesKey)
            throws NoSuchAlgorithmException, InvalidKeySpecException,
            NoSuchPaddingException,
            InvalidKeyException, BadPaddingException,
            IllegalBlockSizeException {
        // 读取公钥
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(rsaPublicKey);
        PublicKey publicKey = keyFactory.generatePublic(keySpec);

        // 利用公钥加密
        Cipher keyCipher = Cipher.getInstance(keyFactory.getAlgorithm());
        keyCipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return keyCipher.doFinal(aesKey);
    }

    private static byte[] rsaPrivateKeyEncrypt(byte[] rsaPrivateKey,
                                               byte[] aesKey)
            throws NoSuchAlgorithmException, InvalidKeySpecException,
            NoSuchPaddingException,
            InvalidKeyException, BadPaddingException,
            IllegalBlockSizeException {
        // 读取私钥
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(rsaPrivateKey);
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

        // 利用私钥加密
        Cipher keyCipher = Cipher.getInstance(keyFactory.getAlgorithm());
        keyCipher.init(Cipher.ENCRYPT_MODE, privateKey);
        return keyCipher.doFinal(aesKey);
    }

    private static byte[] rsaPrivateDecrypt(byte[] privateKey, byte[] secretKey)
            throws NoSuchAlgorithmException, InvalidKeySpecException,
            NoSuchPaddingException, InvalidKeyException,
            BadPaddingException, IllegalBlockSizeException {
        // 读取私钥
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey);
        PrivateKey key = keyFactory.generatePrivate(keySpec);

        // 使用私钥解密
        Cipher keyCipher = Cipher.getInstance(keyFactory.getAlgorithm());
        keyCipher.init(Cipher.DECRYPT_MODE, key);
        return keyCipher.doFinal(secretKey);
    }

    private static byte[] rsaPublicDecrypt(byte[] publicKey, byte[] secretKey)
            throws NoSuchAlgorithmException, InvalidKeySpecException,
            NoSuchPaddingException, InvalidKeyException,
            BadPaddingException, IllegalBlockSizeException {
        // 读取公钥
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
        PublicKey key = keyFactory.generatePublic(keySpec);

        // 使用公钥解密
        Cipher keyCipher = Cipher.getInstance(keyFactory.getAlgorithm());
        keyCipher.init(Cipher.DECRYPT_MODE, key);
        return keyCipher.doFinal(secretKey);
    }

    private static byte[] aesEncrypt(byte[] aesKey, byte[] sourceData)
            throws NoSuchPaddingException, NoSuchAlgorithmException,
            InvalidKeyException, BadPaddingException,
            IllegalBlockSizeException {
        SecretKeySpec keySpec = getAesKeySpec(aesKey);

        // 利用加密器进行加密原文
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);
        return cipher.doFinal(sourceData);
    }

    private static byte[] aesDecrypt(byte[] aesKey, byte[] secretData)
            throws NoSuchAlgorithmException, NoSuchPaddingException,
            InvalidKeyException,
            BadPaddingException, IllegalBlockSizeException {
        SecretKeySpec keySpec = getAesKeySpec(aesKey);

        // 利用加密器进行加密原文
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, keySpec);
        return cipher.doFinal(secretData);
    }

    private static SecretKeySpec getAesKeySpec(byte[] aesKey) throws NoSuchAlgorithmException {
        // 生成SecretKeySpec用于加解密
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        // 在Linux中使用AES算法时，必须要通过SHA1PRNG方式创建SecureRandom对象，否则解密会失败。
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed(aesKey);
        keyGenerator.init(128, secureRandom);
        SecretKey key = keyGenerator.generateKey();
        return new SecretKeySpec(key.getEncoded(), keyGenerator.getAlgorithm());
    }

    /**
     * 创建公钥私钥匙
     *
     * @return Map<String, String>
     * @throws Exception 异常
     */
    public static Map<String, String> createKeys() throws Exception {
        // 为RSA算法创建一个KeyPairGenerator对象
        KeyPairGenerator kpg = KeyPairGenerator.getInstance(RSA_KEY_ALGORITHM);
        // 初始化KeyPairGenerator对象,密钥长度
        kpg.initialize(KEY_SIZE);
        // 生成密匙对
        KeyPair keyPair = kpg.generateKeyPair();
        // 得到公钥
        Key publicKey = keyPair.getPublic();
        // 得到私钥
        Key privateKey = keyPair.getPrivate();
        Map<String, String> keyPairMap = new HashMap<>();

        keyPairMap.put(PUBLIC_KEY,
                ENCODER.encodeToString(publicKey.getEncoded()));
        keyPairMap.put(PRIVATE_KEY,
                ENCODER.encodeToString(privateKey.getEncoded()));
        return keyPairMap;
    }
}
